package com.example.docsearcher.search;

import com.example.docsearcher.Config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Index {
    // 索引文件存储的路径
    private static String INDEX_PATH = null;

    // 根据开关来判断是否为线上环境
    static {
        if(Config.IS_ONLINE){
            INDEX_PATH = "";
        } else {
            INDEX_PATH = "D:\\program\\idea-ultimate\\api\\jdk-8u461-docs-all\\";
        }
    }
    // 针对两个存储索引的变量创建锁对象，保证线程安全，由于创建正倒排索引更改的变量不同，所以这里可以创建两个不同的锁对象，提高速度
    private Object locker1 = new Object();
    private Object locker2 = new Object();


    private ObjectMapper objectMapper = new ObjectMapper();
    // 将索引存贮在哈希表和链表中，放在内存里，查找时间复杂度都为O(1), 提高了查找的效率
    // 将正排索引存储在顺序表中，根据下标存储在对应位置中
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 将倒排索引存储在哈希表中,key是分词结果，对应一组倒排拉链（包含一组docId)
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 从正排索引中获取文章信息
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }

    // 根据分词结果查倒排索引
    public ArrayList<Weight> getInverted(String key){
        return invertedIndex.get(key);
    }

    // 向索引中新增一条文档
    public void addDoc(String title, String url, String content){
        System.out.println("新增索引：" + title);
        // 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 构建倒排索引
        buildInverted(docInfo);

    }

    private void buildInverted(DocInfo docInfo) {
        // 构造一个类，描述标题和文章分词后每个词语出现的次数
        class WordCnt{
            public int titleCount;
            public int contentCount;
        }

        // 1存储分词后每个词语出现的次数
        // 以下两个变量无需加锁，每个不是公共变量，每个线程都会创建自己的对应变量，不会导致线程安全问题
        HashMap<String, WordCnt> wordCntMap = new HashMap<>();
        // 对标题进行分词
        List<Term> termTitle = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        // 2将标题的分词结果存入HashMap中
        for (Term term : termTitle) {
            String termName = term.getName();
            WordCnt wordCnt = wordCntMap.get(termName);
            // 若不存在这个分词结果，则创建一个键值对进行插入
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                wordCntMap.put(termName, newWordCnt);
            } else {
                // 若存在，则直接将原本的值+1进行插入
                wordCnt.titleCount++;
                wordCntMap.put(termName, wordCnt);
            }
        }

        // 3 对文章内容进行分词
        List<Term> termContent = ToAnalysis.parse(docInfo.getContent()).getTerms();
        // 4利用分词结果对短语出现的次数进行更新
        for (Term term : termContent) {
            String termName = term.getName();
            WordCnt wordCnt = wordCntMap.get(termName);
            // 若不存在则创建一个新的键值对，将contentCount设为1，插入HashMap中
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.contentCount = 1;
                wordCntMap.put(termName, newWordCnt);
            } else {
                // 若存在则将contentCount的值加1
                wordCnt.contentCount++;
                wordCntMap.put(termName, wordCnt);
            }
        }

        // 5最终一个词语对应的权重公式为 标题中出现的次数 * 10 + 文章中出现的次数
        // 6对该文章对应的分词结果词语出现的次数HashMap进行遍历，并存进倒排索引表中
        for(HashMap.Entry<String, WordCnt> entry : wordCntMap.entrySet()){
            Weight weight = new Weight();
            weight.setDocId(docInfo.getDocId());
            weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
            // 倒排拉链,对于类变量的修改可能导致线程安全问题，这里加锁
            synchronized (locker1){
                // 从倒排索引表中查找是否有这个词语
                ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());
                // 若没有则构建一个键值对进行插入
                if(invertedList == null){
                    // 插入该词语在倒排索引中对应的拉链
                    invertedList = new ArrayList<>();
                    invertedIndex.put(entry.getKey(), invertedList);
                }
                // 若有的话就将刚刚构造的weight插入倒排拉链中
                invertedList.add(weight);
            }
        }
    }

    public DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        // 约定 forwardIndex 中存储正排索引的id是对应顺序表的下标位置
        // 这里存储的下标就是顺序表的长度
        docInfo.setDocId(forwardIndex.size());
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (locker2){
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }


    // 由于文件过多，这里将文件进行序列化与反序列化存储进硬盘与内存中，使用JSON库来实现

    // 将内存中的文件序列化为JSON格式存储进硬盘当中
    // 将内存中的索引文件存储到硬盘中
    public void save(){

        long beg = System.currentTimeMillis();
        System.out.println("开始保存索引");
        File file = new File(INDEX_PATH);
        // 若文件不存在则创建文件
        if(!file.exists()){
            file.mkdirs();
        }
        // 创建对应的索引文件
        File forwardFile = new File(INDEX_PATH + "forward.txt");
        File invertedFile = new File(INDEX_PATH + "inverted.txt");

        // 使用JSON库中的objectMapper对象将正排索引和倒排索引的数据结构写入到硬盘中
        try{
            objectMapper.writeValue(forwardFile, forwardIndex);
            objectMapper.writeValue(invertedFile, invertedIndex);
        } catch (IOException e){
            e.printStackTrace();
        }
        System.out.println("保存索引结束");
        long end = System.currentTimeMillis();
        System.out.println("保存索引用时：" + (end - beg));
    }

    // 将硬盘中JSON格式的文件进行反序列化加载到内存当中
    // 从硬盘中加载索引文件
    public void load(){
        Long beg =  System.currentTimeMillis();
        System.out.println("开始加载索引");
        File forwardFile = new File(INDEX_PATH + "forward.txt");
        File invertedFile = new File(INDEX_PATH + "inverted.txt");

        // 创建一个匿名内部类来承载要解析的数据类型
        try{
            forwardIndex = objectMapper.readValue(forwardFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e){
            e.printStackTrace();
        }
        System.out.println("加载索引结束");
        long end = System.currentTimeMillis();
        System.out.println("加载索引用时：" + (end - beg));
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("    ");
    }
}
